Skip to content

Refactored docs for std::fs::set_permissions_nofollow + fix BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW flag - #160170

Open
asder8215 wants to merge 4 commits into
rust-lang:mainfrom
asder8215:docs_set_perms_nofollow
Open

Refactored docs for std::fs::set_permissions_nofollow + fix BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW flag#160170
asder8215 wants to merge 4 commits into
rust-lang:mainfrom
asder8215:docs_set_perms_nofollow

Conversation

@asder8215

@asder8215 asder8215 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

View all comments

This PR refactors documentations for std::fs::set_permissions_nofollow and fixes BSD-based systems + Android to use fchmodat with AT_SYMLINK_NOFOLLOW flag (instead of no flag set) and refactors all other platforms to defer to OpenOptions with O_NOFOLLOW behavior.

r? @clarfonthey
Since they looked at the original set_permissions_nofollow PR I made

cc @RalfJung

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 29, 2026
Comment thread library/std/src/fs.rs Outdated
Comment on lines +3452 to +3454
/// * `open` with `O_NOFOLLOW` flag enabled + `fchmod` on WASI.
/// * `fchmodat` function with the flag `AT_SYMLINK_NOFOLLOW` enabled
/// on Unix platforms
/// on Unix platforms.

@clarfonthey clarfonthey Jul 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't super necessary but 🤷🏻 doesn't matter too much

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is grammatically still odd -- the first two bullets are not actually sentences, but the last one is.

@clarfonthey

Copy link
Copy Markdown
Contributor

@bors r+ rollup

@rust-bors

rust-bors Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📌 Commit d24bee2 has been approved by clarfonthey

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 29, 2026
Comment thread library/std/src/fs.rs Outdated
/// * `fchmodat` function with the flag `AT_SYMLINK_NOFOLLOW` enabled
/// on Unix platforms
/// on Unix platforms.
/// * The flag `FILE_FLAG_OPEN_REPARSE_POINT` is enabled and then the

@RalfJung RalfJung Jul 29, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That flag is enabled where? Should this say "SetFileInformationByHandle with the flag ..." or would that not be right?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The internal code for Windows' set_permissions_nofollow does the following:

pub fn set_perm_nofollow(p: &WCStr, perm: FilePermissions) -> io::Result<()> {
    let mut opts = OpenOptions::new();
    opts.access_mode(c::FILE_WRITE_ATTRIBUTES);
    // `FILE_FLAG_OPEN_REPARSE_POINT` for no_follow behavior
    opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS | c::FILE_FLAG_OPEN_REPARSE_POINT);
    let file = File::open_native(p, &opts)?;
    file.set_permissions(perm)
}

It opens the file first with FILE_FLAG_BACKUP_SEMANTICS and FILE_FLAG_OPEN_REPARSE_POINT and then it sets the permissions on the reparse point itself using SetFileInformationByHandle. Enabled is probably a poor choice of word here; at the time, I wasn't too sure how to discuss the opening a file behavior on Windows because it seems like OpenOptions::open doesn't go in depth about platform-specific behavior and the open_native call seem to be doing a lot here:

fn open_native(path: &WCStr, opts: &OpenOptions) -> io::Result<File> {
let creation = opts.get_creation_mode()?;
let sa = c::SECURITY_ATTRIBUTES {
nLength: size_of::<c::SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: ptr::null_mut(),
bInheritHandle: opts.inherit_handle as c::BOOL,
};
let handle = unsafe {
c::CreateFileW(
path.as_ptr(),
opts.get_access_mode()?,
opts.share_mode,
if opts.inherit_handle { &sa } else { ptr::null() },
creation,
opts.get_flags_and_attributes(),
ptr::null_mut(),
)
};
let handle = unsafe { HandleOrInvalid::from_raw_handle(handle) };
if let Ok(handle) = OwnedHandle::try_from(handle) {
if opts.freeze_last_access_time || opts.freeze_last_write_time {
let file_time =
c::FILETIME { dwLowDateTime: 0xFFFFFFFF, dwHighDateTime: 0xFFFFFFFF };
cvt(unsafe {
c::SetFileTime(
handle.as_raw_handle(),
core::ptr::null(),
if opts.freeze_last_access_time { &file_time } else { core::ptr::null() },
if opts.freeze_last_write_time { &file_time } else { core::ptr::null() },
)
})?;
}
// Manual truncation. See #115745.
if opts.truncate
&& creation == c::OPEN_ALWAYS
&& api::get_last_error() == WinError::ALREADY_EXISTS
{
// This first tries `FileAllocationInfo` but falls back to
// `FileEndOfFileInfo` in order to support WINE.
// If WINE gains support for FileAllocationInfo, we should
// remove the fallback.
let alloc = c::FILE_ALLOCATION_INFO { AllocationSize: 0 };
set_file_information_by_handle(handle.as_raw_handle(), &alloc)
.or_else(|_| {
let eof = c::FILE_END_OF_FILE_INFO { EndOfFile: 0 };
set_file_information_by_handle(handle.as_raw_handle(), &eof)
})
.io_result()?;
}
Ok(File { handle: Handle::from_inner(handle) })
} else {
Err(Error::last_os_error())
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say something like

/// This function currently corresponds to the following underlying operations:
/// * WASI: `open` with `O_NOFOLLOW` followed by `fchmod`.
/// * Unix: `fchmodat` with `AT_SYMLINK_NOFOLLOW`.
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
///   by `SetFileInformationByHandle`.

@asder8215 asder8215 Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took a look at Unix implementation of set_permissions_nofollow again, and I wanted to correct something I said about MacOS/BSD-based systems. Currently, they call fchmodat with no flag provided (0), and I realized that's a mistake I made since I forgot to include them underneath the fchmodat with AT_SYMLINK_NOFOLLOW.

pub fn set_perm_nofollow(p: &CStr, perm: FilePermissions) -> io::Result<()> {
// ESP-IDF and Horizon do not support O_NOFOLLOW, so we skip setting it.
// Their filesystems do not have symbolic links, so no special handling is required.
cfg_select! {
// wasm32-wasip1 targets do not support fchmodat, so we fall down to
// open + fchmod
target_os = "wasi" => {
use crate::fs::OpenOptions;
use crate::fs::Permissions;
use crate::os::wasi::ffi::OsStrExt;
use crate::os::wasi::fs::OpenOptionsExt;
let mut options = OpenOptions::new();
options.custom_flags(libc::O_NOFOLLOW);
let bytes = p.to_bytes();
let os_str = OsStr::from_bytes(bytes);
options.open(Path::new(os_str))?.set_permissions(Permissions::from_inner(perm))
}
all(target_os = "linux", not(any(target_os = "espidf", target_os = "horizon"))) => {
cvt_r(|| unsafe {
libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, libc::AT_SYMLINK_NOFOLLOW)
})
.map(|_| ())
},
_ => {
cvt_r(|| unsafe {
libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, 0)
})
.map(|_| ())
}
}
}

I think the reason why I did that was because CI mentioned compiler errors on libc::AT_SYMLINK_NOFOLLOW not existing on certain platforms (though this was on dist-various-1 so wasn't sure what that tested), so I gated it to linux, but forgot to update it to include MacOS/other BSD-based systems that support AT_SYMLINK_NOFOLLOW.

I have to fix this and include the BSD-based systems in that block. I also verified the behavior on my MacBook and it should definitely change the permission bits, which means the test case that sets a symlink with readonly permission should pass.

@RalfJung

Copy link
Copy Markdown
Member

@bors r-

Sorry I have a question :)

@rust-bors rust-bors Bot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. labels Jul 29, 2026
@rust-bors

rust-bors Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This pull request was unapproved.

View changes since this unapproval

@clarfonthey

Copy link
Copy Markdown
Contributor

r? RalfJung since you're mostly reviewing already

@rustbot

rustbot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

RalfJung is not on the review rotation at the moment.
They may take a while to respond.

@asder8215
asder8215 force-pushed the docs_set_perms_nofollow branch from d24bee2 to 82ff1cb Compare July 29, 2026 21:45
@asder8215
asder8215 requested a review from RalfJung July 29, 2026 21:46
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 29, 2026
@asder8215 asder8215 changed the title Refactored docs for std::fs::set_permissions_nofollow clarifying BSD-based platforms behavior Refactored docs for std::fs::set_permissions_nofollow + fix BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW flag Jul 29, 2026
@asder8215
asder8215 force-pushed the docs_set_perms_nofollow branch from 82ff1cb to f0403c2 Compare July 29, 2026 21:49
Comment thread library/std/src/fs/tests.rs Outdated
@@ -662,13 +662,10 @@ fn set_get_permissions_nofollows_symlink() {

@RalfJung RalfJung Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android and Linux share the same kernel, so I highly doubt they will behave different here?

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take it off from this block and we can put up a try job to confirm

Comment thread library/std/src/fs/tests.rs Outdated
// on symlinks could lead to no effect, so we should expect
// there being no change to BSD-based systems.
// on symlinks could lead to no effect, so it's case by case
// on whether the permissions are set

@RalfJung RalfJung Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

            // On these systems, the symlink itself is now marked readonly.

This is meant to replace the entire comment, github is just incapable of making that a suggestion...

View changes since the review

Comment thread library/std/src/sys/fs/unix.rs Outdated
options.open(Path::new(os_str))?.set_permissions(Permissions::from_inner(perm))
}
all(target_os = "linux", not(any(target_os = "espidf", target_os = "horizon"))) => {
all(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "android"), not(any(target_os = "espidf", target_os = "horizon"))) => {

@RalfJung RalfJung Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is going on here? The not part at the end makes no sense at all...?

View changes since the review

Comment thread library/std/src/sys/fs/unix.rs Outdated
Comment on lines 2064 to 2068
// These platforms do not have `AT_SYMLINK_NOFOLLOW` but support fchmodat,
// so no flag is set for fchmodat.
cvt_r(|| unsafe {
libc::fchmodat(libc::AT_FDCWD, p.as_ptr(), perm.mode, 0)
})

@RalfJung RalfJung Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We promise that we will not follow the symlink. So why is it correct to not set the flag?

View changes since the review

@asder8215 asder8215 Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest with you, I wasn't sure what Unix platforms didn't support AT_SYMLINK_NOFOLLOW/O_NOFOLLOW because they didn't support symlinks.

For example, I think in my previous PR someone was commenting that ESPIDF/Horizon OS do not have support O_NOFOLLOW or symlinks, so I figured that their behavior should match with just regularly setting permissions on the file (fchmodat with no flag set). I had the not condition from earlier because I wasn't exhaustively sure which Unix-based platform has no symlinks.

I could try separating the not portion of the second branch into its own branch using fchmodat with no flag set and then have the _ branch do the same thing that lolbinarycat did originally:

use crate::fs::OpenOptions;
use crate::os::unix::fs::OpenOptionsExt;

OpenOptions::new().custom_flags(libc::O_NOFOLLOW).open(path)?.set_permissions(perm)

^This does return a different error message than Unsupported on symlinks though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in my previous PR someone was commenting that ESPIDF/Horizon OS do not have support O_NOFOLLOW or symlinks, so I figured that their behavior should match with just regularly setting permissions on the file (fchmodat with no flag set)

If we are sure this is correct (ping the target maintainers) then we can skip the flag on those targets.

But your code skips the flag on all unknown targets and that doesn't seem good.

OpenOptions::new().custom_flags(libc::O_NOFOLLOW).open(path)?.set_permissions(perm) seems like a good fallback impl.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding ESPIDF/Horizon OS, I found the PR that made the change after lolbinarycat's implementation:
#145746

It's confirmed in this issue that ESPIDF has no libc::O_NOFOLLOW because its supported filesystems don't use symbolic links

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The maintainer of the ESP-IDF target here: yes, I confirm ESP-IDF (and likely Horizon) do not support neither fchmodat, nor AT_SYMLINK_NOFOLLOW, nor O_NOFOLLOW so they should fall back to the old code before #158168. The latest code surface of this PR seems to fix that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, this only makes sense because they don't support symlinks at all. We have to guarantee that these operations never follow symlinks.

Comment thread library/std/src/fs.rs Outdated
/// // or succeed in modifying the permissions of a symlink
/// // This should result in an error on certain platforms,
/// // succeed in modifying the permissions of a symlink,
/// // or do nothing at all.

@RalfJung RalfJung Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know any platform where it does nothing at all?

View changes since the review

@asder8215 asder8215 Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an exhaustive list. All I read from this MacOS post is that it's possible some systems can't change symbolic link permissions at all, which I interpreted to mean two different things: that fchmodat has no effects on symlinks or it throws an error. I did see this issue here that mentions that certain kernel silently ignored AT_SYMLINK_NOFOLLOW, which change permissions on the target, but that does something instead of no effect and it's an incorrect implementation.

Should I assume that AT_SYMLINK_NOFOLLOW should succeed in setting the permission bits/throw an error if it's not supported on symlinks? I'm unsure if no effects on symlinks and not throwing an error is valid behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I'll remove the no effects behavior note. I don't think of it being a valid behavior for this function, and since I can't find any results that tell me what platforms does nothing for fchmodat with AT_SYMLINK_NOFOLLOW

@RalfJung

Copy link
Copy Markdown
Member

The user-facing comments are fine but this is turning into a libs discussion about the implementation.

r? libs

@rust-log-analyzer

This comment has been minimized.

@clarfonthey

Copy link
Copy Markdown
Contributor

@bors delegate try

@rust-bors

rust-bors Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

✌️ @asder8215, you can now perform try builds on this pull request!

You can now post @bors try to start a try build.

@asder8215
asder8215 force-pushed the docs_set_perms_nofollow branch from 4cab1c1 to 14a655b Compare August 17, 2026 02:04
@rust-log-analyzer

This comment has been minimized.

…d fchmodat platform call on fchmodat and every platform falls back to open + fchmod when _res is set to ErrorKind::Unsupported; updated docs to reflect change
@asder8215
asder8215 force-pushed the docs_set_perms_nofollow branch from 14a655b to b89b539 Compare August 17, 2026 04:24
@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try jobs=x86_64-msvc-1,dist-various-*,test-various,aarch64-apple,dist-android

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 19, 2026
Refactored docs for `std::fs::set_permissions_nofollow` + fix BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW flag


try-job: x86_64-msvc-1
try-job: dist-various-*
try-job: test-various
try-job: aarch64-apple
try-job: dist-android
@rust-bors rust-bors Bot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 19, 2026
@rust-bors

rust-bors Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

💔 Test for b7cba8f failed: CI. Failed job:

@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

A job failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
   Compiling diff v0.1.13
   Compiling citool v0.1.0 (/home/runner/work/rust/rust/src/ci/citool)
    Finished `dev` profile [unoptimized] target(s) in 20.96s
     Running `target/debug/citool calculate-job-matrix`
Run type: TryJob { job_patterns: Some(["x86_64-msvc-1", "dist-various-*", "test-various", "aarch64-apple", "dist-android"]), nolimit: false }
Error: Failed to calculate job matrix

Caused by:
    Patterns `aarch64-apple` did not match any auto jobs
##[error]Process completed with exit code 1.

@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try jobs=x86_64-msvc-1,dist-various-*,test-various,dist-android,aarch64-apple-darwin

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 19, 2026
Refactored docs for `std::fs::set_permissions_nofollow` + fix BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW flag


try-job: x86_64-msvc-1
try-job: dist-various-*
try-job: test-various
try-job: dist-android
try-job: aarch64-apple-darwin
@rust-bors

rust-bors Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

💔 Test for 01bf5b1 failed: CI. Failed job:

@rust-log-analyzer

Copy link
Copy Markdown
Collaborator

A job failed! Check out the build log: (web) (plain enhanced) (plain)

Click to see the possible cause of the failure (guessed by this bot)
   Compiling ureq v3.4.0
   Compiling citool v0.1.0 (/home/runner/work/rust/rust/src/ci/citool)
    Finished `dev` profile [unoptimized] target(s) in 30.51s
     Running `target/debug/citool calculate-job-matrix`
Run type: TryJob { job_patterns: Some(["x86_64-msvc-1", "dist-various-*", "test-various", "dist-android", "aarch64-apple-darwin"]), nolimit: false }
Error: Failed to calculate job matrix

Caused by:
    Patterns `aarch64-apple-darwin` did not match any auto jobs
##[error]Process completed with exit code 1.

@asder8215

Copy link
Copy Markdown
Contributor Author

@bors try jobs=x86_64-msvc-1,dist-various-*,test-various,dist-android

(Unsure why aarch64-apple is not working, it worked in the previous PR I made on set_permissions_nofollow).

@rust-bors

This comment has been minimized.

rust-bors Bot pushed a commit that referenced this pull request Aug 19, 2026
Refactored docs for `std::fs::set_permissions_nofollow` + fix BSD-based systems to use fchmodat with AT_SYMLINK_NOFOLLOW flag


try-job: x86_64-msvc-1
try-job: dist-various-*
try-job: test-various
try-job: dist-android
@clarfonthey

Copy link
Copy Markdown
Contributor

I generally just check the latest commit on main to see what the full list of jobs is. There are other ways, but, that's one of the easy ones I do it.

@rust-bors

rust-bors Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

☀️ Try build successful (CI)
Build commit: 5a55929 (5a5592949ddd5fca3a64ce3f68519dce280db83e)
Base parent: e71c0f1 (e71c0f1e3395b10a8c331317be1a5c107bdf7b2e)

// When O_NOFOLLOW flag is enabled, if the trailing component of
// a path is a symbolic link, open should fail with ELOOP error.
// For consistency with other Linux distributions, we return
// `ErrorKind::Unsupported`.

@RalfJung RalfJung Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment. Which "other distributions"? Do different Linuxes behave different here, despite all using the same kernel? Which system returns "unsupported" to indicate a symlink?

View changes since the review

@asder8215 asder8215 Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Rachel's comment earlier. Ubuntu 20.04 throws ENOTSUP when using fchmodat with AT_SYMLINK_NOFOLLOW regardless if the file is a symlink or not. For that platform specifically, we have to use open with O_NOFOLLOW flag and then fchmod. open with O_NOFOLLOW would throw an ELOOP error in this scenario.

Maybe consistency was a bad choice of word here, but I thought it was better to keep the error message to what fchmodat would fail with (ENOTSUPP/Unsupported) instead of using ELOOP here (other reason why is because FilesystemLoop error is unstable and Unsupported is stable, so maybe convenient for those to error handle this without needing nightly features).

@RalfJung RalfJung Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't that just mean that Ubuntu 20 has an old kernel that does not yet support fchmodat? We then trigger the fallback behavior anyway so the user never sees that. What kind of consistency are you trying to achieve here...?

@asder8215 asder8215 Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fchmodat with AT_SYMLINK_NOFOLLOW in later versions of Ubuntu functions normally with setting permissions on regular files/directories and throws Unsupported on symlinks.

And the consistency thing is bad wording on my end. I might've even wrote that comment before getting rid of the cfg_select branches because we had other Unix/Linux platforms solely calling fchmodat and other platforms just calling fchmodat with fallback behavior to open and fchmod on Unsupported error. Now, I just have every platform that supports fchmodat call fchmodat and everything will fall to use open and fchmod.

I'll make changes to the comment in an amended commit later today.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fchmodat with AT_SYMLINK_NOFOLLOW in later versions of Ubuntu functions normally with setting permissions on regular files/directories and throws Unsupported on symlinks.

I assume not just later Ubuntu, but all recent Linux distros? Ubuntu uses a fairly standard kernel AFAIK?

The man page just says

     AT_SYMLINK_NOFOLLOW
            If path is a symbolic link, do not dereference it: instead operate on the link itself.

so it is a bit odd that we get "unsupported" at all...

@fs-rachel fs-rachel Aug 19, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this comment. Which "other distributions"? Do different Linuxes behave different here, despite all using the same kernel? Which system returns "unsupported" to indicate a symlink?

View changes since the review

I wrote that comment, what I meant is that:

  1. On Ubuntu 24.04 (the ideal path), fchmodat() with AT_SYMLINK_NOFOLLOW works on normal files, and returns ENOTSUP if the target is a symlink
  2. On Ubuntu 20.04 (which we, at least, need support for), fchmodat() with AT_SYMLINK_NOFOLLOW always returns ENOTSUP
  3. On both versions, openat() + fchmod() with AT_SYMLINK_NOFOLLOW works on normal files, and returns ELOOP if the target is a symlink

So the remapping is to make case (3) consistent with case (1), in terms of what error we return to the caller

As @asder8215 says, the code has been restructured since I wrote that, so the wording might need revising, but that's the idea.

Comment thread library/std/src/fs.rs
Comment on lines +3452 to +3460
/// * Linux, BSD-based platforms, Android, QNX: `fchmodat` with `AT_SYMLINK_NOFOLLOW`
/// with a fallback behavior to use `open` with `O_NOFOLLOW` followed by behavior
/// denoted in [`fs::set_permissions`] when the former `fchmodat` call errors with `ENOTSUP`[^1].
/// * Other Unix-based platforms with symlinks: `open` with `O_NOFOLLOW` followed by behavior
/// denoted in [`fs::set_permissions`].
/// * Other Unix-based platforms without symlinks: `open` followed by behavior
/// denoted in [`fs::set_permissions`].
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
/// by `SetFileInformationByHandle`.

@RalfJung RalfJung Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// * Linux, BSD-based platforms, Android, QNX: `fchmodat` with `AT_SYMLINK_NOFOLLOW`
/// with a fallback behavior to use `open` with `O_NOFOLLOW` followed by behavior
/// denoted in [`fs::set_permissions`] when the former `fchmodat` call errors with `ENOTSUP`[^1].
/// * Other Unix-based platforms with symlinks: `open` with `O_NOFOLLOW` followed by behavior
/// denoted in [`fs::set_permissions`].
/// * Other Unix-based platforms without symlinks: `open` followed by behavior
/// denoted in [`fs::set_permissions`].
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
/// by `SetFileInformationByHandle`.
/// * Linux, BSD-based platforms, Android, QNX: `fchmodat` with `AT_SYMLINK_NOFOLLOW`. If that is not supported, we fall back to
/// `open` with `O_NOFOLLOW` followed by [`fs::set_permissions`].
/// * Other Unix-based platforms with symlinks: `open` with `O_NOFOLLOW` followed by [`fs::set_permissions`].
/// * Other Unix-based platforms without symlinks: `open` followed by [`fs::set_permissions`].
/// * Windows: `CreateFileW` with `FILE_FLAG_OPEN_REPARSE_POINT` followed
/// by `SetFileInformationByHandle`.

View changes since the review

@RalfJung

Copy link
Copy Markdown
Member

As an extra twist, the glibc manpages, even for (current) Arch and for Ubuntu 26.04, say that this should not work at all! E.g. the Ubuntu 26.04 manpages say in the "errors" section:

https://manpages.ubuntu.com/manpages/resolute/man2/fchmodat.2.html#errors

ENOTSUP
    (fchmodat()) flags specified AT_SYMLINK_NOFOLLOW, which is not supported.

So I think ideally on Linux we should try the fast path but fall back to open() + fchmod() if it fails, to maintain support for older distros. And potentially send a bug report about the man pages not matching the actual behaviour.

I think the man page just says "if you see that error code, it means the flag is not supported".
It does not say "it will always return that error".
It's confusingly worded, but that makes most sense as interpretation here I think.

@fs-rachel

Copy link
Copy Markdown
Contributor

As an extra twist, the glibc manpages, even for (current) Arch and for Ubuntu 26.04, say that this should not work at all! E.g. the Ubuntu 26.04 manpages say in the "errors" section:
https://manpages.ubuntu.com/manpages/resolute/man2/fchmodat.2.html#errors

ENOTSUP
    (fchmodat()) flags specified AT_SYMLINK_NOFOLLOW, which is not supported.

So I think ideally on Linux we should try the fast path but fall back to open() + fchmod() if it fails, to maintain support for older distros. And potentially send a bug report about the man pages not matching the actual behaviour.

I think the man page just says "if you see that error code, it means the flag is not supported". It does not say "it will always return that error". It's confusingly worded, but that makes most sense as interpretation here I think.

I agree, that's the only reading that's consistent with the actual behaviour

@fs-rachel

fs-rachel commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

@fs-rachel Added the fallback behavior in the latest commit (and updated the docs to mention this for Linux platforms). I'll let the reviewers go through it to see if the current behavior is okay.
Change for fallback behavior I took reference from your patch, so listed you as co-author for it.

Thank you :)

I've also noticed that the set_get_permissions_nofollows_symlink fails on QNX. Turns out fchmodat on a symlink succeeds there, but the test expects it to fail.

Rather than adding QNX (and NTO) to the giant list of platforms where we expect fchmodat to succeed, maybe it would be better to invert the condition so that we have a list of platforms where we expect it to fail. That would be shorter and IMHO a lot clearer.

Following up on this, because I realized I wasn't very clear:

The code currently in the main branch, and this patch, both fail on QNX. The tests treat it as a platform where fchmodat on a symlink should fail (like Linux), when actually it succeeds (like Windows and BSD).

I've written a fix and tested that it works on our CI here: ferrocene/ferrocene@a450413 . I don't love how long the list of platforms in that any() is, but I'm not sure that there's a better option with the way the code is currently written.

@asder8215 Please can you cherry-pick that commit into this PR? (minus the "Test" label because I've confirmed that it works now)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.